Skip to main content

pacenotes_installer\wizard\scene/
configuration.rs

1use windows_sys::Win32::Foundation::{HWND, LPARAM, WPARAM};
2use windows_sys::Win32::UI::Controls as ctl;
3use windows_sys::Win32::UI::WindowsAndMessaging as wm;
4
5/// `(list, label)` per list, in `CHOICE_CATEGORIES` order, so index i is the model's list i.
6const LISTS: [(u16, u16); 3] = [
7    (
8        crate::wizard::control_ids::IDC_S5_LANG,
9        crate::wizard::control_ids::IDC_S5_LANG_LABEL,
10    ),
11    (
12        crate::wizard::control_ids::IDC_S5_VOICE,
13        crate::wizard::control_ids::IDC_S5_VOICE_LABEL,
14    ),
15    (
16        crate::wizard::control_ids::IDC_S5_STYLE,
17        crate::wizard::control_ids::IDC_S5_STYLE_LABEL,
18    ),
19];
20
21/// Scene 5: one pick each of language, co-driver and style.
22pub unsafe extern "system" fn configuration_page(
23    dialog: HWND,
24    message: u32,
25    wparam: WPARAM,
26    lparam: LPARAM,
27) -> isize {
28    match message {
29        wm::WM_INITDIALOG => {
30            crate::wizard::page::adopt_state(dialog, lparam);
31            for ((_, label), category) in LISTS.iter().zip(pacenotes_shared::CHOICE_CATEGORIES) {
32                crate::ui::set_text(dialog, *label, category_caption(category));
33            }
34            1
35        }
36        wm::WM_COMMAND => {
37            if crate::ui::hiword(wparam) == crate::ui::LBN_SELCHANGE {
38                on_choice_picked(dialog, crate::ui::loword(wparam));
39            }
40            1
41        }
42        wm::WM_NOTIFY => match crate::wizard::page::notify_code(lparam) {
43            Some(ctl::PSN_SETACTIVE) => {
44                crate::wizard::page::set_sheet_caption(dialog);
45                crate::wizard::page::set_header(dialog, crate::i18n::IDS_S5_TITLE);
46                crate::wizard::page::set_buttons(dialog, ctl::PSWIZB_BACK | ctl::PSWIZB_NEXT);
47                crate::wizard::page::set_next_text(dialog, crate::i18n::IDS_BTN_INSTALL);
48                populate_lists(dialog);
49                crate::wizard::page::reply(dialog, 0)
50            }
51            Some(ctl::PSN_WIZNEXT) => on_install(dialog),
52            Some(ctl::PSN_WIZBACK) => on_back(dialog),
53            Some(ctl::PSN_QUERYCANCEL) => crate::wizard::page::query_cancel(dialog),
54            _ => 0,
55        },
56        _ => 0,
57    }
58}
59
60/// Rebuilt on every activation, since the user may have gone back and changed the ticks.
61unsafe fn populate_lists(dialog: HWND) {
62    let Some(state) = crate::wizard::page::state(dialog) else {
63        return;
64    };
65    let keep = state.config.selected_keys();
66    let categories = pacenotes_installer::schema::choice_categories(state.game_id());
67    state.config =
68        pacenotes_installer::choices::Configuration::from_tree(&state.tree, &keep, &categories);
69
70    let nothing_ticked = crate::i18n::load_string(crate::i18n::IDS_S5_NONE);
71    for (&(list_control, _), list) in LISTS.iter().zip(&state.config.lists) {
72        crate::ui::list_reset(dialog, list_control);
73        if list.options.is_empty() {
74            // Disabled, so the explanation cannot be mistaken for an option.
75            crate::ui::list_add(dialog, list_control, &nothing_ticked);
76            crate::ui::enable(dialog, list_control, false);
77            continue;
78        }
79        crate::ui::enable(dialog, list_control, true);
80        for option in &list.options {
81            crate::ui::list_add(dialog, list_control, &option.label);
82        }
83        crate::ui::list_select(dialog, list_control, list.selected);
84    }
85}
86
87unsafe fn on_choice_picked(dialog: HWND, control: u16) {
88    let Some(index) = LISTS.iter().position(|&(list, _)| list == control) else {
89        return;
90    };
91    let selection = crate::ui::list_selection(dialog, control);
92    if let Some(state) = crate::wizard::page::state(dialog) {
93        state.config.select(index, selection);
94    }
95}
96
97/// Back to where the route came from, which is neither route's previous page:
98/// a finished transfer cannot be re-entered, and Configure came from scene 3.
99unsafe fn on_back(dialog: HWND) -> isize {
100    let route = crate::wizard::page::state(dialog)
101        .map(|state| state.route)
102        .unwrap_or(crate::wizard::state::Route::Download);
103    let target = match route {
104        crate::wizard::state::Route::Download => crate::wizard::control_ids::IDD_S4_DOWNLOAD,
105        crate::wizard::state::Route::Configure | crate::wizard::state::Route::Restore => {
106            crate::wizard::control_ids::IDD_S3_FOLDER
107        }
108    };
109    crate::wizard::page::reply(dialog, target as isize)
110}
111
112/// Every list that offers anything needs a pick; the warning names them by their captions.
113unsafe fn on_install(dialog: HWND) -> isize {
114    let Some(state) = crate::wizard::page::state(dialog) else {
115        return crate::wizard::page::reply(dialog, 0);
116    };
117    let missing = state.config.missing();
118    if missing.is_empty() {
119        return crate::wizard::page::reply(dialog, 0);
120    }
121    let text = crate::i18n::load_string_arg(crate::i18n::IDS_S5_MISSING, &captions(&missing));
122    crate::ui::message_box_warning(dialog, &text);
123    crate::wizard::page::stay(dialog)
124}
125
126/// Scene 5's way in, from either route: one pick per category needs something
127/// on this computer to pick from, so a category with nothing ticked AND cached
128/// is named in a warning, and the caller keeps its page.
129pub(super) unsafe fn has_something_to_pick(
130    dialog: HWND,
131    state: &crate::wizard::state::WizardState,
132) -> bool {
133    let choices = pacenotes_installer::schema::choice_categories(state.game_id());
134    let empty = state
135        .tree
136        .categories_not_ready(&choices, &|key| state.cache.holds(key));
137    if empty.is_empty() {
138        return true;
139    }
140    let text = crate::i18n::load_string_arg(crate::i18n::IDS_S5_NOT_READY, &captions(&empty));
141    crate::ui::message_box_warning(dialog, &text);
142    false
143}
144
145/// The categories as the lists are labelled, for a warning that names them.
146fn captions(categories: &[&str]) -> String {
147    let named: Vec<String> = categories
148        .iter()
149        .map(|category| {
150            crate::i18n::load_string(category_caption(category))
151                .trim_end_matches(':')
152                .to_string()
153        })
154        .collect();
155    named.join(", ")
156}
157
158/// Matched by category rather than position, so a reordered list cannot be mislabelled.
159pub(super) fn category_caption(category: &str) -> u16 {
160    match category {
161        pacenotes_shared::CATEGORY_LANGUAGE => crate::i18n::IDS_S5_LANGUAGE,
162        pacenotes_shared::CATEGORY_CODRIVER => crate::i18n::IDS_S5_VOICE,
163        pacenotes_shared::CATEGORY_STYLE => crate::i18n::IDS_S5_STYLE,
164        _ => crate::i18n::IDS_S5_TITLE,
165    }
166}